La criminalidad en el mundo ha sido un tema a tratar desde el inicio de la sociedad. Los estados y países de todo el mundo se han propuesto bajar los índices de delitos, ya que hoy en día es imposible evitar cualquier clase de delito, pero esto no siempre se puede concretar.
Él data set que tenemos a disposición para el análisis representa las características de algunos de los llamados de emergencia registrados, los cuales son meritorios de llamarse delitos, por ende, fueron atendidos por la policía local.
En el siguiente data set tomaremos como labor principal la inspección de los datos recopilados en este data set, el análisis y la detección de problemáticas relacionadas a estos llamados.
Data set: https://www.kaggle.com/datasets/jgiigii/uscrimesdataset
En la siguiente sección veremos de manera básica de que se trata los datos recopilados dentro de este data set, esto con el fin de detectar anomalías y problemas, los cuales podemos solucionar eventualmente.
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import plotly.express as px
from wordcloud import WordCloud
na_values = [' ']#?
# Input data files are available in the read-only "../input/" directory
# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory
data = pd.read_csv('https://gitlab.com/c.godoy09/mineria_datos/-/raw/main/Crimes_With_Dates_Cleaned.csv',
na_values = na_values)
En primera instancia, veremos la totalidad de filas y columnas, a manera de cuantificar la cantidad de datos que tenemos a disposición de análisis.
rows = len(data.axes[0])
cols = len(data.axes[1])
print("Número de filas: " + str(rows))
print("Número de columnass: " + str(cols))
data.head(3)
Se puede observar que existe una columna sin datos ni nombre, por ende se eliminará.
data.drop(labels = ["Unnamed: 0"], inplace = True, axis = 1)
Mostramos las primeras 3 filas de nuestra data set, ya con la columna sin nombre eliminada
data.head(3)
Mostramos todas las columnas de nuestro data set y de que tipo corresponde cada variable
# Tipo de dato de cada columna
data.dtypes
Las variables presentes en la colección se clasifican como variables cuantitativas y variables cualitativas incluyendo el detalle de cada una de ellas como se muestra a continuación.
1.- Variables cuantitativas
2.- Variables Cualitativas
Se realiza un "split" en la variable Dispach Date Time (mes día y hora), la cual corresponde a la fecha exacta en la cual se acudió a la emergencia. De esto obtenemos:
Lo anterior se realiza con el objetivo de poseer variables que ayuden al análisis
data["date"] = pd.to_datetime(data["Dispatch Date / Time"])
data.drop(labels = ["Dispatch Date / Time"], axis = 1, inplace = True)
data["month"] = data["date"].apply(lambda x: x.month_name())
data["day_name"] = data["date"].apply(lambda x: x.day_name())
data["hour"] = data["date"].apply(lambda x: x.hour)
data.head(2)
Para saber si el dataset cumple es viable es necesario saber si posee missing values, óseas celdas que no poseen valor. También debemos saber que atributos no cumplen o no aportan valores a nuestro trabajo.
Ahora se listará los atributos junto a la contabilización que presentan de valores nulos o blancos.
data.isnull().sum()
import seaborn as sns
# Identificamos los missing values visualmente
sns.heatmap(data.isnull(), cbar=False)
Se puede observar que variables como dispach_date_time, BlockAdress, Street Prefix y End_date_time, por mencionar algunas, presentan gran cantidad datos considerados como N/A o que no han sido registrados, por ejemplo, Street Suffix presenta 300662 datos nulos o en blanco de un total de 306094 registros aproximadamente. Producto de lo anterior las nuevas variables generadas (mont day_name y year) poseen 49029 datos nulos
Se eliminarán variables de poca relevancia para nuestro análisis, bajo los siguientes criterios:
Street Suffix,Street Prefix y Street Type: son datos irrelevantes además de contener bastantes datos nulos o en blanco, por ende se considera que el valor que pueden aportar al análisis es de poco interés.Street name: la información que aporta también a la la variable Block Adress y más completa para efectos de nuestro proyectoAgency: es información irrelevante, ya que la información aportada con esta variable es la misma que Police District Name, pero de forma más escueta
-Cr Number: Representa solo un ID del caso, misma información que obtenemos con Incident IDStart_Date_Timey End_Date_Time: representa fechas y horas de inicio del caso y fin, ambas variables son poco relevantes, ya que Dispach_Date_Time representa de mejor manera la información que necesitamos para abordar nuestra problemática.Latitude, Longitude, Place y PRA: la dimension espacial se utiliza mejor mediante la variable Location.Year-Month: para representar el mes y el año, se utiliza month y yearBasándose en la sección anterior, se procede a eliminar los atributos que no aportan o se encuentran con número grande de valores NA, y también se precede a eliminar los valores NA.
## Eliminación de atributos
data = data.drop(columns=["Agency","CR Number","Street Name","Street Suffix","Street Prefix","Street Type","Start_Date_Time","NIBRS Code","Latitude","Longitude","End_Date_Time","Place","PRA","Beat","Year-Month","Police District Number"])
data.dropna(subset=['Crime Name1', 'Crime Name2', 'Crime Name3','Police District Name','Block Address','City','Address Number','Sector', 'date', 'Month', 'day_name','hour','Zip Code'], inplace=True)
data.isnull().sum()
sns.heatmap(data.isnull(), cbar=False)
Realizando una sumatoria de todos los valores NA existentes en el dataset, se puede ver que no existen valores NA. De la misma forma mediante el diagrama es posible ver que no existe presencia de estos valores.
rows = len(data.axes[0])
cols = len(data.axes[1])
print("Número de filas: " + str(rows))
print("Número de columnass: " + str(cols))
Finalmente de poseer 306094 filas y 36 columnas, luego de la limpieza de datos y la agregación de columnas en la sección 2.1 obtenemos un dataset con 229289 filas y 22 columnas
Luego de efectuar el análisis exploratorio de los datos, seguimos con la entrega de la información de manera visual, de esta forma lograremos captar de mejor manera la información entregada por el data set.
data.groupby('City')['Victims'].count().sort_values(ascending=False)
Tenemos en el dataset registro de delitos de 54 ciudades, de las cuales luego de la limpieza de datos se utilizan 43 ciudades, en donde:
#Delitos por año y mes
import plotly.express as px
month_order = ["January", "February", "March", "April", "May",
"June", "July", "August", "September",
"October", "November", "December"]
year_order = np.sort(data.Year.unique())
fig = px.histogram(
data, x = "month", barmode = "group",
color = "Year",
category_orders = {"Year": year_order, "month": month_order}
)
fig.show()
A partir del año 2017 existe una disminución de los delitos cometidos, sin embargo, el año 2017 y 2022 poseen una diferencia considerable con los demás años.
Los delitos cometidos durante el año 2016 son registrados desde el mes de julio hasta el mes de diciembre, mientras que el año 2022 los delitos son registrados desde el mes de enero hasta una parte del mes de agosto. Debido a lo anterior estos años poseen una diferencia en sus registros respecto a los otros años.
px.defaults.template = "seaborn"
days_order = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
year_order = np.sort(data.Year.unique())
fig = px.histogram(
data, x = "day_name",
category_orders = {"day_name": days_order, "Year": year_order},
color = "Committed_At_Morning", barmode = "group",
)
fig.show()
En este gráfico se puede observar que el día martes es el día en el cual se reciben más alertas de delitos durante una semana, también es apreciable que los días domingos son los días con menos alertas de delitos registrados en esta muestra, también podemos observar que los delitos son alertados mayormente en el horario de la tarde, con más del doble de alertas, en comparación del horario de la mañana
Ahora se observará de manera más detallada el anterior gráfico, al visualizar la proporción de las alertas por horas y no por jornadas.
days_order = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
d = data
d["hour"] = d["hour"].astype(int).astype(str)
fig = px.histogram(
d, x = "hour", barmode = "group",
category_orders = {
"Year": year_order,
"hour": sorted(range(24))
},
)
fig.add_annotation(
x = 4, y = 4500,
arrowcolor = "black", arrowwidth = 2,
text = "Tendencia baja de crimenes",
)
fig.show()
La actividad delictual expresada en las horas del día, se puede observar una mayor actividad en el rango de las 15 a 16 horas, y la menor actividad se detecta en el rango horario de las 4 a 5 de la madrugada.
El gráfico visualiza de manera eficaz el porcentaje que representa cada uno de los tipos de delitos dentro de la muestra. El mayor porcentaje con un 50.2% de los delitos registrados es en contra de la propiedad, le sigue con un 22.2% de delitos varios, con un 15.7% se acontecen alertas por crímenes contra la sociedad, el 10.7% de las alertas corresponden a crímenes contra la persona y por último, el 1.31% son alertas que no corresponden a ningún crimen en concreto.
fig = px.pie(
data.dropna(subset = ["Crime Name1"]), names = "Crime Name1",
labels={"Crime Name1": "Crime type:"},
title = "Type of crime",
)
fig.update_layout(
title = dict(
font_size = 30,
),
legend = dict(
bgcolor="LightSteelBlue",
font_size = 15
),
)
fig.show()
Teniendo la información de las cantidades y comportamientos del registro de alertas delictivas durante horas y días, nos compete definir a que corresponden estas alertas
En este gráfico observamos la cantidad de llamados de emergencia a lo largo de los años, los cuales fueron categorizados dependiendo del tipo de crimen cometido.
data_groupby_year = data.groupby(['Year', 'Crime Name1']).size().unstack().reset_index(inplace=False)
crimes = ['Crime Against Person', 'Crime Against Property', 'Crime Against Society', 'Not a Crime', 'Other']
data_groupby_year.plot(x='Year', y=crimes, kind="line", figsize=(8, 8))
plt.legend(loc='upper right')
plt.title('Cantidad de Crimenes comentidos por Tipo de Crimen')
plt.show()
plt.clf()
Se observa que la tendencia de las alertas corresponden a los crímenes que atentan contra la propiedad, también se aprecia un declive en las alertas en el último año, esto se puede explicar, ya que el año 2022 aún no termina, por ende los registros se encuentran incompletos
data_group_by_year_crime_2 = data.groupby(['Year', 'Crime Name2'])['Crime Name2'].count()
# plot the result
data_group_by_year_crime_2.unstack().plot()
plt.xticks(rotation=45)
plt.legend(bbox_to_anchor = (0.65, 1.25))
plt.show()
Destacamos que existen un tipo de crimen que destaca por sobre el resto el cual tuvo su frecuencia de más alta en el año 2018 y luego empieza una caída sustancial en esa cantidad de crimenes.
Se pretende ahora definir con mayor detalle la proporción que tiene cada tipo de alerta delictiva dentro del registro de datos
Dada la información obtenida mediante los análisis previos hemos encontrado la siguiente problemática, hemos visto que existen diferencias de la cantidad de delitos cometidos entre día y noche, entre los meses e incluso entre los años, pero nos gustaría saber cómo podemos predecir estos crímenes. Para esto debemos tener en cuenta 2 preguntas:
Luego del análisis efectuado podemos definir y plantear que nuestro experimento será propuesto como un problema de clasificación, tomando el atributo Crime Name1 de nuestro dataset, el cual índica el Tipo de crimen. De este modo intentaremos establecer cuál modelo nos permitirá obtener mejores resultados al momento de clasificar un tipo de crimen. Para esta propuesta se utilizaron los siguientes algoritmos;
De igual forma las variables utilizadas para esta propuesta e implementadas en los modelos serán
Se define la variable target y las columnas usadas para la predicción.
El siguiente caso nuestro target serán los Crime Name1 ya que buscamos predecir esta columna en este método experimental.
Mientras que las variables utilizadas para predecir la columna Crime Name1 son las mencionadas en el Modelo Propuesto.
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import classification_report, confusion_matrix
from sklearn.model_selection import train_test_split
from sklearn import tree
from sklearn.model_selection import GridSearchCV
from sklearn.metrics import classification_report
import numpy as np
TargetVariable='Crime Name1'
Predictors=['Victims', 'Year', 'Month', 'hour', 'Day', 'Committed_At_Morning']
X=data[Predictors].values
y=data[TargetVariable].values
Se define el set de prueba y el de entrenamiento con un 33% de los datos como datos para realizar el testeo de la predicción y que siempre ocupe el mismo random para la evaluación del resultado el cual es la semilla 37.
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=.33, random_state=37)
Ahora se definen los criterios de entropy para el árbol de decisiones y como profundidad máxima del árbol 3 para evitar el sobreajuste del mismo.
#Se define los hiperparametros
clf = DecisionTreeClassifier(max_depth=3,criterion='entropy')
#Printing all the parameters of Decision Trees
print(clf)
#Se crea el modelo de entrenamiento
DTree=clf.fit(X_train,y_train)
prediction=DTree.predict(X_test)
print(classification_report(y_test, prediction))
print(confusion_matrix(y_test, prediction))
Se observa una precisión bastante alta en Crime Against Person, pero no se puede tener certeza de que estos valores nos permitan realmente predecir correctamente todo los crimenes ya que existen 2 categorías en un 0% de predicción por los datos.
En el caso de la matriz de confusión observamos que el que tiene más aciertos es Crime
Para medir el desempeño del modelo utilizaremos cross validation, en este primer caso con la combinación de splits de 10
from sklearn.model_selection import KFold, cross_val_score
k_folds = KFold(n_splits = 10)
scores = cross_val_score(clf, X, y, cv = k_folds)
print(scores)
Como resultado obtenemos una lista que refleja los valores por cada validación de los datos de prueba, donde los valores rondan entre %0 y como máximo %77.2
Ahora en este segundo caso con la combinacion de splits de 15
k_folds = KFold(n_splits = 15)
scores = cross_val_score(clf, X, y, cv = k_folds)
print(scores)
Como resultado obtenemos una lista que refleja los valores por cada validación de los datos de prueba, donde los valores rondan entre %0 y como máximo %77.3
Creamos nuestro X e y de entrada y los sets de entrenamiento y test a partir de los atributos que utilicemos.
# knn
from sklearn.preprocessing import MinMaxScaler
from sklearn.neighbors import KNeighborsClassifier
X = data[['Victims', 'Year', 'Month', 'hour', 'Day', 'Committed_At_Morning']].values
y = data['Crime Name1'].values
Entrenamiento del dataset a partir del target y la data
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=40)
scaler = MinMaxScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)
Creacion del clasificador knn, a partir de
knn = KNeighborsClassifier(10)
knn.fit(X_train, y_train)
print('Accuracy of K-NN classifier on training set: {:.2f}'
.format(knn.score(X_train, y_train)))
print('Accuracy of K-NN classifier on test set: {:.2f}'
.format(knn.score(X_test, y_test)))
pred = knn.predict(X_test)
print(confusion_matrix(y_test, pred))
print(classification_report(y_test, pred))
Para medir el desempeño del modelos utilizaremos cross validation, ya que nos permitira testear el modelo a partir de la cantidad de segmetaciones que utilicemos
from sklearn.model_selection import KFold, cross_val_score
k_folds = KFold(n_splits = 10)
scores = cross_val_score(knn, X, y, cv = k_folds)
print(scores)
Como resultado obtenemos una lista que refleja los valores por cada validación de los datos de prueba, donde los valores como minimo son de 5% y como máximo un 46%
Si bien se logró compara los resultados de la clasificación entre Árbol de decisiones y KNN se observa un mejor resultado en el método de Árbol de decisiones, pero no son concluyentes a responder las problemáticas ya que sus valores no son muy representativos en cuanto a todos los resultados posibles en el problema. Ademas podemos concluir que nuestro dataset no posee datos equilibrados en cuanto al atributo que queremos predecir y tambien en posible que se necesite aplicar otras tecnicas mas avanzadas para obtener resultados mas alentadores.